{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "## Softmax 函数与交叉熵函数\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "---"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "#### 介绍"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "本实验主要讲解了分类问题中的二分类问题和多分类问题之间的区别，以及每种问题下的交叉熵损失的定义方法。由于多分类问题的输出为属于每个类别的概率，要求概率和为 1 。因此，我们还介绍了如何利用 Softmax 函数，处理神经网络的输出，使其满足损失函数的格式要求。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "#### 知识点"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "- 二分类和多分类\n",
    "\n",
    "- 交叉熵损失\n",
    "\n",
    "- PyTorch 中的 Softmax 和交叉熵\n",
    "\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "---"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### 二分类问题和多分类问题"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "二分类问题：表示分类任务有两个类别。比如我们想要识别一副图是否是猫，我们一般会训练出一个分类器，输入一副图片（用向量 x 表示），输出该图片是猫的概率 p。我们可以使用 max 函数判断 p 和 0.5 的大小。如果 p 大，则输出 1（表示该图像为猫）。如果 0.5 大，则输出 0 （表示该图像不为猫）。这就是二分类问题，即输出只为 0 或 1 的分类问题。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "多分类问题：表示分类任务有多个类别。比如我们需要建立一个分类器，用于分辨一堆水果图片中，哪些是橘子、哪些是苹果还有哪些是香蕉。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "在二分类问题中，我们可以使用 max（代码描述为 `if a > b return a; else b`） 来判断结果，就是非黑即白。但是在多分类问题中，我们就不能这样做。我们需要引入 Softmax 的概念。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "#### Softmax"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "在机器学习尤其是深度学习中，Softmax 是个非常常用的函数，尤其在多分类的场景中使用广泛。他把输入映射为 0-1 之间的实数，并且通过归一化保证和为 1。\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "在多分类问题中，我们需要分类器输出每种分类的概率，且为了能够比较概率之间的大小，我们还希望概率之和能够为 1。因此，我们就需要使用 Softmax 函数。 特别是在利用神经网络解决多分类问题时，我们一般都会将输出的最后一层，加上 Softmax 函数，用于规则化输出。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "假设一个数组为 $V$，$v_i$ 表示 $V$ 中的第 $i$ 个元素，那么这个元素经历了 Softmax 函数后的输出为："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "$$S_i = \\frac{e^{v_i}}{\\sum_{i=0}^Ne^{v_i}}$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "这个定义其实很简单，也就是对输入的值进行了指数化，我认为这里进行指数化的目的是为了扩大任意两个输入之间的差距。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "将指数化后的值除以总的值，目的是将所有的值缩放到 0-1 之间，并保证所有的输出值相加的和为 1 。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "我们可以先利用 NumPy 对其进行实现："
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "source": [
    "# softmax就是把每个输出x的每项取指数(目的是消除负数),然后再每项除以项数的总和，这样形成的一个概率分布。\r\n",
    "import numpy as np\r\n",
    "\r\n",
    "\r\n",
    "def softmax(x):\r\n",
    "    return np.exp(x) / np.sum(np.exp(x), axis=0)\r\n",
    "\r\n",
    "\r\n",
    "x = np.array([2.0, 1.0, 0.1])\r\n",
    "outputs = softmax(x)\r\n",
    "\r\n",
    "print('numpy 版 softmax 的输入 :', x)\r\n",
    "print('numpy 版 softmax 的输出 :', outputs)\r\n",
    "print('numpy 版 softmax 的输出之和:', outputs.sum())"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "numpy 版 softmax 的输入 : [2.  1.  0.1]\n",
      "numpy 版 softmax 的输出 : [0.65900114 0.24243297 0.09856589]\n",
      "numpy 版 softmax 的输出之和: 1.0\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "source": [
    "import numpy as np\r\n",
    "\r\n",
    "\r\n",
    "def softmax(x):\r\n",
    "    return np.exp(x) / np.sum(np.exp(x))\r\n",
    "\r\n",
    "\r\n",
    "x = np.array([2.0, -1.0, 0.1])\r\n",
    "print(softmax(x))"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "[0.83378101 0.04151151 0.12470747]\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "当然，我们也可以使用 PyTorch 中自带的 Softmax 函数，完成数据的处理："
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "source": [
    "import torch\r\n",
    "import torch.nn as nn\r\n",
    "\r\n",
    "x = torch.tensor([2.0, 1.0, 0.1])\r\n",
    "outputs = torch.softmax(x, dim=0)  # dim=0，表示处理的是第 1 维的数据\r\n",
    "print('torch 版 softmax 的输入 :', x)\r\n",
    "print('torch 版 softmax 的输出 :', outputs)\r\n",
    "print('torch 版 softmax 的输出之和:', outputs.sum())"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "torch 版 softmax 的输入 : tensor([2.0000, 1.0000, 0.1000])\n",
      "torch 版 softmax 的输出 : tensor([0.6590, 0.2424, 0.0986])\n",
      "torch 版 softmax 的输出之和: tensor(1.0000)\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "source": [
    "import torch\r\n",
    "from torch import nn\r\n",
    "\r\n",
    "x = torch.tensor([2.0, 1.0, 0.1])\r\n",
    "outputs = torch.softmax(x, dim=0)\r\n",
    "print(outputs)"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "tensor([0.6590, 0.2424, 0.0986])\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### 损失函数"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "损失函数反映的是预测结果和实际结果之间的差距，即从预测结果到实际结果需要走的距离，即所需要消耗的成本，故称之为损失函数。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "这里让我们介绍一种常用的损失函数：交叉熵损失。那么为什么我们需要使用交叉熵损失作为我们的损失函数呢？为什么我们不直接用错误率来作为损失函数，用梯度下降算法得到错误率最低时的模型呢？为了能够更好的阐述上面这个问题，让我们来举个例子。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "#### 葡萄酒的种类预测"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "我们希望通过葡萄酒的酒精浓度 、苹果酸浓度 、灰分浓度等独立特征，来预测该葡萄酒源产地。假设数据集中有三种源产地：英国、法国和美国。 "
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "假设这里我们建立了两个模型用以预测葡萄酒的种类。每个模型都会输出三个值，即输入的葡萄酒来源于英国、法国和美国的概率。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "这里我们对两个模型输入了三条相同的数据，得到的结果如下："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "模型 1 ："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "<img width=\"600px\" src=\"https://doc.shiyanlou.com/courses/2534/1166617/c7a87f94b4866e58db0a7d6a337d1857-0/wm\">"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "从结果可以看出，模型 1 对于样本 1 和样本 2 以非常微弱的优势（概率只比其他结果高 \n",
    "0.1）判断正确，对于样本 3 的判断则彻底错误。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "模型 2 ："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "<img width=\"600px\" src=\"https://doc.shiyanlou.com/courses/2534/1166617/69151732a0da460908543eba65b422fa-0/wm\">"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "模型 2 对于样本 1 和样本 2 的判断非常准确（概率比其他结果高很多）。\n",
    "\n",
    "模型 2 对于样本 3 的判断错误，但是相对来说没有错得太离谱（概率只比其他结果高 0.1）。\n",
    "\n"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "好了，有了模型之后，我们需要通过定义损失函数来判断模型在样本上的表现，那么我们可以定义哪些损失函数呢？"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "如果使用简单的分类错误率作为损失函数，那么两个模型的分类错误率为："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "模型 1："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "$$classification\\_error = \\frac{1}{3}$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "模型 2："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "$$classification\\_error = \\frac{1}{3}$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    " 从结果可以看出，如果使用分类错误率来衡量两个模型的好坏，那么这两个模型的好坏程度相同。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "我们从上面的结果可以看出，虽然模型 1 和模型 2 都预测错了 1 个，但是相对来说，模型 2 的预测效果更好，损失函数照理来说应该更小。因此，我们使用分类错误率不能很好的描述模型的优劣。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "为此，我们引入了交叉熵损失函数用以描述模型的优劣。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "#### 交叉熵损失函数"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "交叉熵损失函数有两种形式：二分类形式和多分类形式。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "在 **二分类的任务** 中，模型最后需要预测的结果只有两种情况，对于每个类别，我们的预测得到的概率为 $p$ 和 $1-p$ 。此时的交叉熵损失（又叫二进制交叉熵）为："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "$$L = \\frac{1}{N}\\sum_iL_i=\\frac{1}{N}\\sum_i-[y_i\\cdot log(p_i)+(1-y_i)\\cdot log(1-p_i)]$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "其中："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "- $y_i$：表示样本 $i$ 的真实标签值，正类为 1，负类为 0。\n",
    "- $p_i$：表示样本 $i$ 的预测为正的概率。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "当然，我们不必手动实现上面的损失函数。我们可以利用 `nn.BCELoss()` 定义二值交叉熵损失。如下："
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "source": [
    "loss = nn.BCELoss()\r\n",
    "# 假设两个模型最后的预测结果相同，但是概率不同\r\n",
    "model_one_pred = torch.tensor([0.9, 0.6])\r\n",
    "model_two_pred = torch.tensor([0.6, 0.9])\r\n",
    "# 真实结果\r\n",
    "target = torch.FloatTensor([1, 0])\r\n",
    "\r\n",
    "# 计算两种模型的损失\r\n",
    "l1 = loss(model_one_pred, target)\r\n",
    "l2 = loss(model_two_pred, target)\r\n",
    "l1, l2"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "(tensor(0.5108), tensor(1.4067))"
      ]
     },
     "metadata": {},
     "execution_count": 5
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "source": [
    "# 二元交叉熵其实就是Sigmoid，分类交叉熵是二元交叉熵的推广\r\n",
    "loss = nn.BCELoss()\r\n",
    "# 假设模型的预测标签结果相同但概率不同\r\n",
    "model_one_pred = torch.tensor([0.9,0.6])\r\n",
    "model_two_pred = torch.tensor([0.8,0.6])\r\n",
    "# 真实结果\r\n",
    "target = torch.FloatTensor([1,0])\r\n",
    "# 计算损失\r\n",
    "l1 =  loss(model_one_pred,target)\r\n",
    "l2 =  loss(model_two_pred,target)\r\n",
    "print(f'l1:{l1:},l2:{l2:}')"
   ],
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "l1:0.5108256936073303,l2:0.5697171688079834\n"
     ]
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "从上面代码中可以看出，其实模型 1 和模型 2 都预测对了一条数据，预测错了一条数据。但是，由于模型 1 是在概率差距很大的情况下，预测正确的。因此，模型 1 的预测效果比模型 2 好，即模型 1 的损失应当比模型 2 的损失小。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "在 **多分类任务** 中，交叉熵损失的函数形式会发生一定的改变（其实就是二进制交叉熵损失的扩展）："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "$$L=\\frac{1}{M}\\sum_iL_i=-\\frac{1}{M}\\sum_{c=1}^My_{ic}log(p_{ic})$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "其中："
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "- $M$: 类别的数量。\n",
    "- $y_{ic}$: 标签的 one-hot 编码，如果该类别和样本 i 的类别相同，就是 1，否则为 0。\n",
    "- $p_{ic}$: 对于观察样本 i 属于类别 c 的预测概率。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "利用 PyTorch 中的 `nn.CrossEntropyLoss()` 定义多分类任务的交叉熵损失函数。但是，实际上 `nn.CrossEntropyLoss()` 是包含了 `nn.LogSoftmax()` 和 `nn.NLLLoss()` 。 因为这里已经是概率值了，所以我们使用 `nn.NLLLoss()` 来计算交叉熵损失。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "接下来让我们利用交叉熵损失来评估一下上面建立的两个葡萄酒预测模型的好坏："
   ],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "source": [
    "loss = nn.NLLLoss()\r\n",
    "# 三条数据的真实结果：法国、美国、英国\r\n",
    "Y = torch.tensor([2, 1, 0])\r\n",
    "\r\n",
    "# 模型一对每条数据的预测，每条数据对应三个概率，表示该条数据属于第 i 类的概率值\r\n",
    "model_one_pred = torch.tensor(\r\n",
    "    [[0.3, 0.3, 0.4],  # predict class 2\r\n",
    "     [0.3, 0.4, 0.3],  # predict class 1\r\n",
    "     [0.1, 0.2, 0.7]])  # predict class 0\r\n",
    "\r\n",
    "# 模型 2 对每条数据的预测，每条数据对应三个概率，表示该条数据属于第 i 类的概率值\r\n",
    "model_two_pred = torch.tensor(\r\n",
    "    [[0.1, 0.2, 0.7],  # predict class 2\r\n",
    "     [0.1, 0.7, 0.2],  # predict class 1\r\n",
    "     [0.4, 0.3, 0.3]])  # predict class 0\r\n",
    "l1 = loss(torch.log(model_one_pred), Y)\r\n",
    "l2 = loss(torch.log(model_two_pred), Y)\r\n",
    "l1, l2"
   ],
   "outputs": [],
   "metadata": {}
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "source": [
    "# 也就是说模型最后一层如果加了softmax那么需要用nllloss算loss\r\n",
    "# 如果模型最后一层不加softmax，那么可以用nn.CrossEntropyLoss()一步到位\r\n",
    "loss = nn.NLLLoss()\r\n",
    "Y =  torch.tensor([2,1,0])\r\n",
    "# 模型一对每条数据的预测，每条数据对应三个概率，表示该条数据属于第 i 类的概率值\r\n",
    "model_one_pred = torch.tensor(\r\n",
    "    [[0.3, 0.3, 0.4],  # predict class 2\r\n",
    "     [0.3, 0.4, 0.3],  # predict class 1\r\n",
    "     [0.1, 0.2, 0.7]])  # predict class 0\r\n",
    "\r\n",
    "# 模型 2 对每条数据的预测，每条数据对应三个概率，表示该条数据属于第 i 类的概率值\r\n",
    "model_two_pred = torch.tensor(\r\n",
    "    [[0.1, 0.2, 0.7],  # predict class 2\r\n",
    "     [0.1, 0.7, 0.2],  # predict class 1\r\n",
    "     [0.4, 0.3, 0.3]])  # predict class 0\r\n",
    "l1 = loss(torch.log(model_one_pred), Y)\r\n",
    "l2 = loss(torch.log(model_two_pred), Y)\r\n",
    "l1, l2\r\n",
    "# 其实就是想说，用准确率的话只用到了对或错个数的信息，并没有用到输出的概率分布的信息，只要求分对\r\n",
    "# 而用交叉熵能够捕捉到输出概率分布的差异，更为精确，不但要求分对，而且要求概率分布还要接近\r\n",
    "# 同时使用交叉熵能够加快模型的收敛速度"
   ],
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "(tensor(1.3784), tensor(0.5432))"
      ]
     },
     "metadata": {},
     "execution_count": 10
    }
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "从上面的损失大小可以看出，通过交叉熵损失对模型进行评估的话，模型 2 的效果要优于模型 1 的效果，这是符合我们的客观想法的。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "综上，这就是我们为什么使用交叉熵损失的原因。交叉熵损失函数除了考虑模型的准确率之外，还将模型的鲁棒性等因素考虑了进去，能够更好的评价模型的好坏，使训练出来的模型具有更加稳定的预测准确率。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "从上面的输入可以看出，交叉熵损失需要的的输入是每条数据所属种类的概率，且这些概率之和为 1。说到这里，我想你已经联想到了本实验开始学到的 Softmax 函数了吧。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "实际上，我们一般无法严格的将神经网络的输出控制在 0 - 1 之间，更无法使这些值之和等于 1。因此我们一般会在神经网络的最后一层，加上 softmax 函数，得到每种种类的概率值。然后将概率值放入交叉熵损失函数之中，得到预测结果和真实结果之间的距离。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "### 实验总结"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "本实验以葡萄酒的类别为预测的例，以一种简单的方式，理解了交叉熵损失与其他损失的不同，以及引入交叉熵损失的原因。当然，从模型的训练角度来讲，引入交叉熵损失函数也是为了能够加快模型的收敛速度。"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "<hr><div style=\"color: #999; font-size: 12px;\"><i class=\"fa fa-copyright\" aria-hidden=\"true\"> 本课程内容版权归蓝桥云课所有，禁止转载、下载及非法传播。</i></div>"
   ],
   "metadata": {}
  }
 ],
 "metadata": {
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3.8.0 64-bit ('pytorch': conda)"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.0"
  },
  "interpreter": {
   "hash": "95edf26445b41d81dc60008cc593bb3c243ca80a3a822915e2b6f7013280bc10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}